﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.Serialization;
using System.Xml;
using System.IO;

namespace LFractalLib
{
    public class Entity
    {
        // The points (x1, y1), (x2, y2) are in screen reference frame i.e. X grows left to right, but y grows top to bottom
        public delegate void LineDrawHandler( double x1, double y1, double x2, double y2);
        public delegate void RenderStartHandler(Entity E);
        public delegate void RenderEndHandler(Entity E);
        
        public event LineDrawHandler RenderLineToCanvas = null;
        public event RenderStartHandler RenderStart = null;
        public event RenderEndHandler RenderEnd = null;


        private string name = "";
        private string axiom = "";
        private CollectionWithNotification<ProductionRule> productionRules = new CollectionWithNotification<ProductionRule>();
        private Dictionary<char, int> alphabetTable = new Dictionary<char, int>();
        
        /* Entity designer can put thier initial turtle position/length/angle preferences.
         * But renderer may choose to ignore it
         */
        private double x0 = 0.5, y0;   // Origin in a 1x1 box reference. The renderer will map it to actual canvas size

        /// <summary>
        /// Initial x position of turtle. This should be between 0 to 1, inclusive
        /// </summary>
        public double X0
        {
            get { return x0; }
            set { x0 = (value>1)?1.0d:value; }
        }

        /// <summary>
        /// Initial y position of turtle. This should be between o to 1, inclusive
        /// </summary>
        public double Y0
        {
            get { return y0; }
            set { y0 = (value > 1) ? 1.0d : value; }
        }

        private double armLength = 50; // length of segment

        /// <summary>
        /// Preferred initial length of line segment
        /// </summary>
        public double ArmLength
        {
            get { return armLength; }
            set { armLength = value; }
        }


        private double alpha = 90;  // initial turtle angle

        /// <summary>
        /// Initial angle (in degree) of turtle
        /// </summary>
        public double Alpha
        {
            get { return alpha; }
            set { alpha = value; }
        }

        private double delta = 90; // angle to move for every + or - command

        /// <summary>
        /// The angular (in degree) turn to take for each + or - command
        /// </summary>
        public double Delta
        {
            get { return delta; }
            set { delta = value; }
        }

        private double armScalingFactor = -0.75;

        /// <summary>
        /// Scaling factor for arm length after each level. Positive value means
        /// length will grow by this factor, negative means length will reduce by this factor
        /// </summary>
        public double ArmScalingFactor
        {
            get { return armScalingFactor; }
            set { armScalingFactor = value; }
        } 



        public Dictionary<char, int> AlphabetTable
        {
            get { return alphabetTable; }
        }

        public CollectionWithNotification<ProductionRule> ProductionRules
        {
            get { return productionRules; }
        }


        public string Axiom
        {
            get { return axiom; }
            set { axiom = value; }
        }

        public string Name
        {
            get { return name; }
            set { name = value; }
        }

        public void Reset()
        {
            productionRules.Clear();
            alphabetTable.Clear();
            axiom = "";
        }

        public Entity(string sName)
        {
            this.name = sName;
            this.ProductionRules.ItemAdded += new CollectionWithNotification<ProductionRule>.OnItemAddHandler(ProductionRules_ItemAdded);
            this.ProductionRules.ItemRemoving += new CollectionWithNotification<ProductionRule>.OnItemBeingRemovedHandler(ProductionRules_ItemRemoving);
        }

        void ProductionRules_ItemRemoving(ProductionRule itemBeingRemoved)
        {
            if (itemBeingRemoved != null)
            {
                DereferenceAlphabet(itemBeingRemoved.NonTerminal);
                foreach (char ch in itemBeingRemoved.Rule)
                {
                    DereferenceAlphabet(ch);
                }
            }
        }

        void AddToAlphabetSet(char ch)
        {
            if (alphabetTable.ContainsKey(ch))
            {
                alphabetTable[ch]++; // increment the usage counter
            }
            else
            {
                alphabetTable.Add(ch, 0);
            }
        }

        void DereferenceAlphabet(char ch)
        {
            if (alphabetTable.ContainsKey(ch))
            {
                alphabetTable[ch]--;
                if (alphabetTable[ch] <= 0)
                {
                    // Time to remove it
                    alphabetTable.Remove(ch);
                }
            }
        }

        void ProductionRules_ItemAdded(ProductionRule itemAdded)
        {
            // Update our alphabet set depending on the contet of what's just got added
            if (itemAdded != null)
            {
                AddToAlphabetSet(itemAdded.NonTerminal);
                foreach (char ch in itemAdded.Rule)
                {
                    AddToAlphabetSet(ch);
                }
                
            }
        }

        #region Enity Drawing through delegation

        /// <summary>
        /// Traverses the entity definition & rules to n-th level & raises event for client to render the line on any 
        /// canvas/graphics media it wants.
        /// NOTE: This method does not draw by itself, it raises RenderLineToCanvas event 
        /// </summary>
        /// <param name="x0">Origin-X</param>
        /// <param name="y0">Origin-Y</param>
        /// <param name="armLength">Line segment legth for each F command. Note that the armLength is for the requested level.
        /// This method does not compute armLength for the requested level.
        /// </param>
        /// <param name="alpha">starting angle (in degrees) of the turtle. </param>
        /// <param name="beta">the delta angle (in degrees) turn for each + - command</param>
        /// <param name="level">level for which traversal needs to be done</param>
        public void DrawViaDelegate(double x0, double y0, double armLength, double alpha, double beta, int level)
        {
            if (RenderStart != null) RenderStart(this);

            if (RenderLineToCanvas != null) // no point traversing unless there's someone to draw lines
            {
                string sTurtleCommand = ComputeTurtleCommand(level);
                double tx1 = x0, ty1 = y0, angle = alpha * Math.PI / 180.0, delta = beta * Math.PI / 180.0, tx2=0, ty2=0;
                Stack<TurtleState> stack = new Stack<TurtleState>();

                if (sTurtleCommand != null && sTurtleCommand.Length > 0)
                {
                    foreach (char ch in sTurtleCommand)
                    {
                        switch (ch)
                        {
                            case 'F':   // move while drawing forward
                                {
                                    tx2 = tx1 + armLength * Math.Cos(angle);
                                    ty2 = ty1 - armLength * Math.Sin(angle);
                                    RenderLineToCanvas(tx1, ty1, tx2, ty2);
                                    tx1 = tx2;
                                    ty1 = ty2;
                                }
                                break;
                            case 'f':   // move forward
                                {
                                    tx2 = tx1 + armLength * Math.Cos(angle);
                                    ty2 = ty1 - armLength * Math.Sin(angle);
                                    tx1 = tx2;
                                    ty1 = ty2;
                                }
                                break;
                            case '+': // rotate by delta anti-close wise
                                {
                                    angle += delta;
                                }
                                break;
                            case '-': // rotate by delta in close wise direction
                                {
                                    angle -= delta;
                                }
                                break;
                            case '[': // push current turtle position + direction to stack
                                {
                                    TurtleState T = new TurtleState(tx1, ty1, angle);
                                    stack.Push(T);
                                }
                                break;
                            case ']': // pop the last turtle state from stack
                                {
                                    TurtleState T = stack.Pop();
                                    tx1 = T.X0; ty1 = T.Y0; angle = T.Angle;
                                }
                                break;
                        }
                    }
                }
            }

            if (RenderEnd != null) RenderEnd(this);
        }

        public string ComputeTurtleCommand(int level)
        {
            if (level >= 0)
            {
                if (level == 0)
                {
                    return this.Axiom;
                }
                else
                {
                    // Compute the command for one level up... & apply the production rules
                    string s0 = ComputeTurtleCommand(level - 1);
                    StringBuilder sbResult = new StringBuilder(1024);
                    foreach (char ch in s0)
                    {
                        // Is there a production rule with nonterminal as ch ?
                        ProductionRule rule = this.productionRules.InnerList.Find(P => P.NonTerminal == ch);
                        if (rule != null) // we apply only the first rule matched
                        {
                            sbResult.Append(rule.Rule);
                        }
                        else sbResult.Append(ch);
                    }
                    return sbResult.ToString();
                }
            }

            return "";
        }


        #endregion

        #region Serialization / De-serialization
        public static void WriteNodeVale(string nodeName, string value, ref System.Xml.XmlWriter oWriter)
        {
            oWriter.WriteStartElement(nodeName);
            oWriter.WriteString(value);
            oWriter.WriteEndElement();
        }

        /// <summary>
        /// Get XML representation of this entity definition
        /// </summary>
        public string ToXML()
        {
            StringBuilder sbResult = new StringBuilder();
            System.Xml.XmlWriterSettings oSettings = new System.Xml.XmlWriterSettings();
            oSettings.Encoding = Encoding.ASCII;
            oSettings.Indent = true;
            oSettings.OmitXmlDeclaration = true;
            System.Xml.XmlWriter oWriter = System.Xml.XmlWriter.Create(sbResult, oSettings);

            oWriter.WriteStartElement("LSystemEntity");
            WriteNodeVale("Name", this.Name, ref oWriter);
            WriteNodeVale("Axiom", this.axiom, ref oWriter);

            foreach (ProductionRule R in this.ProductionRules)
            {
                R.ToXmlWriter(ref oWriter);
            }

            WriteNodeVale("OriginX", this.x0.ToString(), ref oWriter);
            WriteNodeVale("OriginY", this.y0.ToString(), ref oWriter);
            WriteNodeVale("ArmLength", this.armLength.ToString(), ref oWriter);
            WriteNodeVale("Alpha", this.alpha.ToString(), ref oWriter);
            WriteNodeVale("Delta", this.delta.ToString(), ref oWriter);
            WriteNodeVale("ArmScalingFactor", this.armScalingFactor.ToString(), ref oWriter);

            oWriter.WriteEndElement();
            oWriter.Close();

            return sbResult.ToString();
        }


        public static Entity FromStream(string filePath)
        {
            XmlDocument oDoc = new XmlDocument();
            oDoc.Load(filePath);
            string sName = oDoc.SelectSingleNode("/LSystemEntity/Name").InnerText;
            Entity E = new Entity(sName);
            E.Axiom = oDoc.SelectSingleNode("/LSystemEntity/Axiom").InnerText;
            XmlNodeList oRules = oDoc.SelectNodes("/LSystemEntity/Rule");
            if (oRules != null && oRules.Count > 0)
            {
                foreach (XmlNode R in oRules)
                {
                    char nonTerminal = R.SelectSingleNode("Replace").InnerText[0];
                    string terminal = R.SelectSingleNode("With").InnerText;
                    ProductionRule pr = new ProductionRule(nonTerminal, terminal);
                    E.ProductionRules.Add(pr);
                }

            }

            E.x0 = double.Parse(oDoc.SelectSingleNode("/LSystemEntity/OriginX").InnerText);
            E.y0 = double.Parse(oDoc.SelectSingleNode("/LSystemEntity/OriginY").InnerText);
            E.armLength = double.Parse(oDoc.SelectSingleNode("/LSystemEntity/ArmLength").InnerText);
            E.alpha = double.Parse(oDoc.SelectSingleNode("/LSystemEntity/Alpha").InnerText);
            E.delta = double.Parse(oDoc.SelectSingleNode("/LSystemEntity/Delta").InnerText);
            E.armScalingFactor = double.Parse(oDoc.SelectSingleNode("/LSystemEntity/ArmScalingFactor").InnerText);

            return E;
        }
        #endregion

    }
}
